58. 绘制饼图

本章概要

  • 学习材料:两只中国基金同报告期前十大持仓官方原字段,其中 stk_mkv_ratio 是占股票持仓市值比;以及本地 stock_basic_data.h5/stock_basic_info 当前静态中信一级行业快照。
  • 本章任务:运行 lst-industry-allocation-prepfig-industry-allocation-outputlst-industry-allocation-audit,完成规范化、映射、聚合、选图与检查。
  • 完成后你将得到:官方字段/分母、来源/访问日、映射文件/key/当前分类快照、逐基金占股票市值比、其余股票持仓及两种图形。
  • 自我检查:确认字段齐全、证券均能匹配行业、比例为非负有限值,并且按股票持仓市值计算的行业比例合计为 100%。发布数据前还应确认公开使用权限。
  • 拓展练习:用完全相同的代码整理另一只中国基金的数据,匹配行业分类,并检查比例合计是否为 100%。

饼图:比例的可视化

饼图(Pie Chart)是展示部分与整体关系的经典工具

在金融分析中,饼图帮助我们:

  • 资产配置:展示投资组合的构成
  • 收入结构:主营业务与其他业务的占比
  • 市场份额:不同公司的市场占有率
  • 行业分布:投资的行业配置

饼图的数学基础

饼图将圆划分为若干扇形,每个扇形的角度对应一个类别的比例:

\[ \large \theta_i = \frac{x_i}{\sum_{j=1}^n x_j} \times 360° \]

其中:

  • \(x_i\):第 \(i\) 个类别的数值
  • \(\theta_i\):第 \(i\) 个扇形的角度

运行前预测|平台任务1:创建融资存量数据框

  • 输入预测:运行前先写出 timenamedatasAFRE 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。
  • 结果预测:不展开答案,先预测将得到AFRE 的结果;同时写出方向、数量级或表格/图形结构。
  • 完成要求:能独立说明本任务从输入到“平台任务1:创建融资存量数据框”结果的关键步骤,原样录入平台代码并得到可核对的运行结果。

⭐ 平台任务1:创建融资存量数据框

展开完整代码(投影默认折叠)
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库

import numpy as np  # 导入NumPy数值计算库

time=['2018年12月末','2019年3月末','2019年6月末','2019年9月末']#创建日期的列表

name=['人民币贷款','债券','委托贷款','信托贷款','股票','承兑汇票','贷款核销','外币贷款']#创建指标名称的列表

# 创建NumPy数组datas
datas=np.array([[134.69,140.98,144.71,148.58],[29.25,30.53,31.89,33.54],[12.36,12.15,11.89,11.73],[7.85,7.88,7.88,7.68],[7.01,7.06,7.13,7.24],[3.81,4.01,3.77,3.28],[3.01,3.18,3.43,3.66],

[2.21,2.18,2.21,2.19]])#创建具体数据的数组

AFRE = pd.DataFrame(data=datas,index=name,columns=time)  # 创建数据框AFRE

print(AFRE)  # 输出变量AFRE的值

任务复盘|平台任务1:创建融资存量数据框

运行后核对:核对 timenamedatasAFRE 是否按预测参与运算,实际输出是否与预测一致;若不一致,先检查类型、单位、索引/字段和运算顺序。

拓展练习:把数据替换为一个中国企业或市场序列,改变一个分组或时间窗口;说明图形结论是否改变,以及为何。

运行前预测|平台任务2:绘制基础饼图

  • 输入预测:运行前先写出 timenamedatasAFRE 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。
  • 结果预测:不展开答案,先预测将得到AFRE 的结果;同时写出方向、数量级或表格/图形结构。
  • 完成要求:能独立说明本任务从输入到“平台任务2:绘制基础饼图”结果的关键步骤,原样录入平台代码并得到可核对的运行结果。

⭐ 平台任务2:绘制基础饼图

展开完整代码(投影默认折叠)
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import matplotlib.pyplot as plt  # 导入Matplotlib绘图库
import pandas as pd  # 导入Pandas数据分析库
import numpy as np  # 导入NumPy数值计算库
plt.rcParams["font.sans-serif"] = ["SimHei"]  # 设置Matplotlib全局参数
time=['2018年12月末','2019年3月末','2019年6月末','2019年9月末']#创建日期的列表
name=['人民币贷款','债券','委托贷款','信托贷款','股票','承兑汇票','贷款核销','外币贷款']#创建指标名称的列表
# 创建NumPy数组datas
datas=np.array([[134.69,140.98,144.71,148.58],[29.25,30.53,31.89,33.54],[12.36,12.15,11.89,11.73],[7.85,7.88,7.88,7.68],[7.01,7.06,7.13,7.24],[3.81,4.01,3.77,3.28],[3.01,
3.18,3.43,3.66],[2.21,2.18,2.21,2.19]])#创建具体数据的数组
AFRE = pd.DataFrame(data=datas,index=name,columns=time)  # 创建数据框AFRE
plt.figure(figsize=(9,6))  # 创建图形画布
plt.pie(x=AFRE.iloc[:,-1],labels=AFRE.index,textprops={"fontsize":13}) #绘制2018年末各项融资存量的饼图
plt.axis("equal") #使饼图是一个图形
plt.legend(fontsize=12)  # 添加图例
plt.title(u"2019年9月末各类融资存量的占比",fontsize=13)  # 设置图表标题
plt.show()  # 显示图形
plt.savefig("1.png")  # 保存图形至文件

任务复盘|平台任务2:绘制基础饼图

运行后核对:核对 timenamedatasAFRE 是否按预测参与运算,实际输出是否与预测一致;若不一致,先检查类型、单位、索引/字段和运算顺序。

拓展练习:把数据替换为一个中国企业或市场序列,改变一个分组或时间窗口;说明图形结论是否改变,以及为何。

运行前预测|平台任务3:绘制多子图饼图

  • 输入预测:运行前先写出 timenamedatasAFRE 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。
  • 结果预测:不展开答案,先预测将得到AFRE 的结果;同时写出方向、数量级或表格/图形结构。
  • 完成要求:能独立说明本任务从输入到“平台任务3:绘制多子图饼图”结果的关键步骤,原样录入平台代码并得到可核对的运行结果。

⭐ 平台任务3:绘制多子图饼图

展开完整代码(投影默认折叠)
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import matplotlib.pyplot as plt  # 导入Matplotlib绘图库
import pandas as pd  # 导入Pandas数据分析库
import numpy as np  # 导入NumPy数值计算库
plt.rcParams["font.sans-serif"] = ["SimHei"]  # 设置Matplotlib全局参数
time=['2018年12月末','2019年3月末','2019年6月末','2019年9月末']#创建日期的列表
name=['人民币贷款','债券','委托贷款','信托贷款','股票','承兑汇票','贷款核销','外币贷款']#创建指标名称的列表
# 创建NumPy数组datas
datas=np.array([[134.69,140.98,144.71,148.58],[29.25,30.53,31.89,33.54],[12.36,12.15,11.89,11.73],[7.85,7.88,7.88,7.68],[7.01,7.06,7.13,7.24],[3.81,4.01,3.77,3.28],[3.01,
3.18,3.43,3.66],[2.21,2.18,2.21,2.19]])#创建具体数据的数组
AFRE = pd.DataFrame(data=datas,index=name,columns=time)  # 创建数据框AFRE
plt.figure(figsize=(11,11))  # 创建图形画布
plt.subplot(2,2,1)#第1行、第1列子图
plt.pie(x=AFRE.iloc[:,0],labels=AFRE.index,labeldistance=1.03,counterclock=False,textprops={'fontsize':12})#绘制2018年12月末各项融资存量的饼图
plt.axis('equal')#使饼图是一个圆形
plt.title(u"2018年12月末",fontsize=14)  # 设置图表标题
plt.subplot(2,2,2)#第1行、第2列子图
plt.pie(x=AFRE.iloc[:,1],labels=AFRE.index,labeldistance=1.03,counterclock=False,textprops={'fontsize':12})#绘制2019年3月末各项融资存量的饼图
plt.axis('equal')#使饼图是一个圆形
plt.title(u"2019年3月末",fontsize=14)  # 设置图表标题
plt.subplot(2,2,3)#第2行、第1列子图
plt.pie(x=AFRE.iloc[:,2],labels=AFRE.index,labeldistance=1.03,counterclock=False,textprops={'fontsize':12})#绘制2019年6月末各项融资存量的饼图
plt.axis('equal')#使饼图是一个圆形
plt.title(u"2019年6月末",fontsize=14)  # 设置图表标题
plt.subplot(2,2,4)#第2行、第2列子图
plt.pie(x=AFRE.iloc[:,3],labels=AFRE.index,labeldistance=1.03,counterclock=False,textprops={'fontsize':12})#绘制2019年9月末各项融资存量的饼图
plt.axis('equal')#使饼图是一个圆形
plt.title(u"2019年9月末",fontsize=14)  # 设置图表标题
plt.show()  # 显示图形
plt.savefig("2.png")  # 保存图形至文件

任务复盘|平台任务3:绘制多子图饼图

运行后核对:核对 timenamedatasAFRE 是否按预测参与运算,实际输出是否与预测一致;若不一致,先检查类型、单位、索引/字段和运算顺序。

拓展练习:把数据替换为一个中国企业或市场序列,改变一个分组或时间窗口;说明图形结论是否改变,以及为何。

饼图关键参数解析

参数 说明 示例
autopct 显示百分比格式 '%1.1f%%' 保留1位小数
startangle 起始角度 90 从12点方向开始
explode 突出显示 (0.1, 0, 0) 第一块偏移
labeldistance 标签距圆心距离 1.03 略大于半径
counterclock 是否逆时针 False 顺时针排列

环形图:更现代的饼图

环形图(甜甜圈图)在饼图中心挖空,可放置额外信息

环形图的优势

  • 更现代、美观的视觉效果
  • 中心可放置额外信息(总计、图标等)
  • 减小扇形面积的视觉误导

环形图代码实现

展开完整代码
import matplotlib.pyplot as plt

# ==================== 收入数据 ====================
revenue_sources = ['主营业务', '其他业务', '投资收益', '资产处置']
revenues = [850, 120, 80, 50]  # 各项收入,单位:亿元
colors_revenue = ['#2E86AB', '#008080', '#F0A700', '#E3120B']

# ==================== 绘制环形图 ====================
fig, ax = plt.subplots(figsize=(8, 4.2))

wedges, texts, autotexts = ax.pie(
    revenues, labels=revenue_sources,
    colors=colors_revenue, autopct='%1.1f%%',
    startangle=90, pctdistance=0.85)

# 创建白色圆心,形成环形效果
center_circle = plt.Circle((0, 0), 0.60, fc='white')
ax.add_artist(center_circle)

# 在中心添加总计信息
total_revenue = sum(revenues)
ax.text(0, 0, f'总收入\n{total_revenue}亿元',
        ha='center', va='center', fontsize=16, fontweight='bold')

plt.title('收入构成', fontsize=16, fontweight='bold', pad=20)
plt.tight_layout()
plt.show()
图中展示环形图——收入构成分析;读者应依据坐标、图例与注释比较主要模式。
Figure 1: 环形图——收入构成分析

条形图 vs 饼图:如何选择?

场景 推荐图表 理由
类别≤5,重点是构成 饼图 直观展示比例
类别>5 条形图 避免饼图过于细碎
需要精确比较数值 条形图 长度比角度更易比较
层次结构 环形图/嵌套饼图 展示多级构成
时间序列变化 堆叠条形图 饼图难以展示变化

条形图与饼图对比示例

展开完整代码
import matplotlib.pyplot as plt
import numpy as np

# ==================== 市场份额数据 ====================
companies = ['公司A', '公司B', '公司C', '公司D', '公司E']
market_share = [35, 25, 20, 12, 8]
colors_comparison = ['#E3120B', '#008080', '#F0A700', '#2C3E50', '#8E9EAA']

fig, axes = plt.subplots(1, 2, figsize=(12, 4.2))

# 左:饼图
axes[0].pie(market_share, labels=companies, colors=colors_comparison,
            autopct='%1.1f%%', startangle=90)
axes[0].set_title('饼图', fontsize=16, fontweight='bold')

# 右:条形图
bars = axes[1].bar(companies, market_share,
                   color=colors_comparison, alpha=0.8)
axes[1].set_title('条形图', fontsize=16, fontweight='bold')
axes[1].set_ylabel('市场份额(%)', fontsize=16)
axes[1].grid(axis='y', alpha=0.3)

# 添加数值标签
for bar in bars:
    height = bar.get_height()
    axes[1].text(bar.get_x() + bar.get_width()/2., height,
                f'{height}%', ha='center', va='bottom', fontsize=16)

plt.tight_layout()
plt.show()
图中展示对比——条形图与饼图;读者应依据坐标、图例与注释比较主要模式。
Figure 2: 对比——条形图与饼图

嵌套饼图:多层级数据展示

展开完整代码
import matplotlib.pyplot as plt

# ==================== 外层:大类资产 ====================
major_assets = ['权益类', '固定收益类', '另类投资']
major_alloc = [50, 35, 15]

# ==================== 内层:细分类别 ====================
sub_assets = ['A股', '港股', '国债', '信用债', '商品', 'REITs']
sub_alloc = [30, 20, 20, 15, 8, 7]

fig, ax = plt.subplots(figsize=(10, 4.2))

# 内层环不放置文字,类别与比例统一移入图例避免环内碰撞
inner_wedges, _ = ax.pie(sub_alloc, radius=0.72,
       colors=['#E3120B', '#F0656E', '#008080', '#46A1A8',
               '#F0A700', '#F7C555'],
       startangle=90, wedgeprops={'width': 0.28, 'edgecolor': 'white'})

# 外层饼图
outer_wedges, _ = ax.pie(major_alloc, radius=1.0,
       colors=['#E3120B', '#008080', '#F0A700'],
       startangle=90, wedgeprops={'width': 0.24, 'edgecolor': 'white'})

major_labels = [f'大类|{name} {value}%' for name, value in zip(major_assets, major_alloc)]  # 在图例中绑定大类比例
sub_labels = [f'细分|{name} {value}%' for name, value in zip(sub_assets, sub_alloc)]  # 在图例中绑定细分类别比例
ax.legend([*outer_wedges, *inner_wedges], [*major_labels, *sub_labels], title='资产类别与比例', loc='center left', bbox_to_anchor=(1.02, 0.5), fontsize=16, title_fontsize=16)  # 用单一图例避免两组标题与标签碰撞

plt.title('投资组合多层级构成', fontsize=16, fontweight='bold', pad=20)
plt.tight_layout()
plt.show()
图中展示嵌套饼图——多层级数据展示;读者应依据坐标、图例与注释比较主要模式。
Figure 3: 嵌套饼图——多层级数据展示

金融应用:基金行业配置分析

Listing 1: 两只中国基金持仓与本地行业分类的对应方法
展开完整参考实现
from pathlib import Path  # 导入路径工具以执行本地行业映射数据来源要求
import numpy as np  # 导入数值库以检查各项比例合计
import pandas as pd  # 导入表格库以组织持仓与行业配置
holdings_source='Tushare fund_portfolio原始字段摘录;stk_mkv_ratio=占股票持仓市值比;公告日2025-10-28;访问日2026-08-26'  # 锁定官方字段、分母与访问日期
industry_file=Path('/home/ubuntu/r2_data_mount/data/stock/stock_basic_data.h5')  # 锁定本地证券行业映射文件
industry_key='stock_basic_info'  # 锁定本地行业映射HDF key
industry_snapshot='citics_2019_l1_name(中信一级2019口径);当前静态文件快照访问日2026-08-26;用于映射2025-09-30历史持仓,未验证报告期历史分类'  # 公开当前分类快照映射历史持仓的版本边界
raw_holdings=pd.DataFrame([('000001.OF','20250930','20251028','603986.SH',2.69),('000001.OF','20250930','20251028','601689.SH',2.72),('000001.OF','20250930','20251028','300308.SZ',3.74),('000001.OF','20250930','20251028','002475.SZ',2.72),('000001.OF','20250930','20251028','002371.SZ',2.97),('000001.OF','20250930','20251028','688347.SH',3.80),('000001.OF','20250930','20251028','688041.SH',2.58),('000001.OF','20250930','20251028','002025.SZ',5.38),('000001.OF','20250930','20251028','688019.SH',2.49),('000001.OF','20250930','20251028','600276.SH',3.00),('000011.OF','20250930','20251028','300750.SZ',8.43),('000011.OF','20250930','20251028','300408.SZ',2.83),('000011.OF','20250930','20251028','002475.SZ',4.54),('000011.OF','20250930','20251028','601899.SH',3.26),('000011.OF','20250930','20251028','002371.SZ',2.86),('000011.OF','20250930','20251028','688052.SH',3.11),('000011.OF','20250930','20251028','688617.SH',2.69),('000011.OF','20250930','20251028','688088.SH',3.45),('000011.OF','20250930','20251028','600522.SH',3.05),('000011.OF','20250930','20251028','600276.SH',2.79)],columns=['ts_code','end_date','ann_date','symbol','stk_mkv_ratio'])  # 按Tushare官方字段名录入两基金同报告期前十大持仓摘录
required={'ts_code','end_date','ann_date','symbol','stk_mkv_ratio'}  # 定义Tushare持仓原始字段结构与股票持仓市值分母
if not required.issubset(raw_holdings.columns) or not industry_file.exists(): raise RuntimeError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 文件或字段不全时提示读者检查下载内容
raw_holdings['order_book_id']=raw_holdings['symbol'].str.replace('.SH','.XSHG',regex=False).str.replace('.SZ','.XSHE',regex=False)  # 把Tushare symbol规范为本地order_book_id
if raw_holdings[['ts_code','symbol']].duplicated().any() or not np.isfinite(raw_holdings['stk_mkv_ratio']).all(): raise RuntimeError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 执行唯一与数值要求
stock_basic=pd.read_hdf(industry_file,key=industry_key)  # 读取本地证券基础信息分类快照
map_required={'order_book_id','citics_2019_l1_name'}  # 定义行业映射最小字段结构
if not map_required.issubset(stock_basic.columns): raise RuntimeError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 缺分类字段时停止
stock_map=stock_basic[list(map_required)].drop_duplicates('order_book_id')  # 保留唯一证券与中信一级行业映射
mapped=raw_holdings.merge(stock_map,on='order_book_id',how='left',validate='many_to_one')  # 把两基金持仓按规范代码映射行业
if mapped['citics_2019_l1_name'].isna().any() or mapped['order_book_id'].nunique()!=raw_holdings['order_book_id'].nunique(): raise RuntimeError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 分类缺失或覆盖失败时停止
industry_sum=mapped.groupby(['ts_code','citics_2019_l1_name'],as_index=False)['stk_mkv_ratio'].sum()  # 逐基金聚合前十大持仓的占股票持仓市值比
remaining_stock_share=100-industry_sum.groupby('ts_code')['stk_mkv_ratio'].sum()  # 在股票持仓市值同一分母内计算其余股票持仓
if (remaining_stock_share<0).any(): raise RuntimeError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 比例越界时停止
industry_sum=pd.concat([industry_sum,pd.DataFrame({'ts_code':remaining_stock_share.index,'citics_2019_l1_name':'其余股票持仓','stk_mkv_ratio':remaining_stock_share.values})],ignore_index=True)  # 补齐股票持仓市值整体中未进入前十的部分
stock_industry_share=industry_sum.pivot(index='citics_2019_l1_name',columns='ts_code',values='stk_mkv_ratio').fillna(0.0)  # 形成同一股票持仓市值分母的行业矩阵
assert np.allclose(stock_industry_share.sum(axis=0).to_numpy(),100.0) and (stock_industry_share>=0).all().all()  # 逐基金核对股票持仓市值分母下100%守恒与非负
Listing 2: 行业配置可视化聚合规则
展开可视化聚合规则
top_industries=stock_industry_share.drop(index='其余股票持仓').sum(axis=1).nlargest(4).index  # 按两基金合计占股票市值比选择四个主要行业
display_stock_share=stock_industry_share.reindex(top_industries).copy()  # 保留主要行业用于可读比较
display_stock_share.loc['其余前十持仓行业']=stock_industry_share.drop(index=list(top_industries)+['其余股票持仓']).sum(axis=0)  # 算法聚合其余已披露前十持仓行业
display_stock_share.loc['其余股票持仓']=stock_industry_share.loc['其余股票持仓']  # 保留同一股票持仓市值分母内的其余持仓
assert np.allclose(display_stock_share.sum(axis=0).to_numpy(),100.0)  # 检查可视化聚合后仍在股票持仓市值分母内守恒
industries=display_stock_share.index.tolist()  # 固定图形中的行业顺序,便于比较
x=np.arange(len(industries))  # 生成行业横轴位置
width=0.38  # 设置两只基金的组内柱宽

基金行业配置可视化

展开完整参考实现
import matplotlib.pyplot as plt  # 导入绘图库以呈现行业分类结果
fig, axes = plt.subplots(1, 2, figsize=(12, 4.15),gridspec_kw={'width_ratios':[1,1.35]})  # 在实际发图单元创建稍矮双面板画布并给比较图更多宽度
wedges,_,_=axes[0].pie(display_stock_share['000001.OF'],labels=None,autopct=lambda value:f'{value:.1f}%' if value>=5 else '',startangle=90,textprops={'fontsize':16})  # 在股票持仓市值分母内绘制聚合扇区
axes[0].set_title('000001.OF|本地行业映射结果', fontsize=16, fontweight='bold')  # 标注基金与行业分类结果
axes[0].legend(wedges,[f'{industry} {share:.2f}%' for industry,share in display_stock_share['000001.OF'].items()],loc='upper center',bbox_to_anchor=(.5,-.02),ncol=2,fontsize=13,title='行业与占股票市值比',title_fontsize=13)  # 用外置图例完整绑定同一分母的分类标签
axes[1].barh(x-width/2,display_stock_share['000001.OF'],width,label='000001.OF',color='#2E86AB',alpha=.8)  # 横向绘制基金A占股票持仓市值比
axes[1].barh(x+width/2,display_stock_share['000011.OF'],width,label='000011.OF',color='#E3120B',alpha=.8)  # 横向绘制另一基金占股票持仓市值比
axes[1].set_xlabel('占股票持仓市值比(%)', fontsize=16)  # 标注官方字段分母与量纲
axes[1].set_ylabel('行业', fontsize=16)  # 标注行业轴
axes[1].set_title('同报告期、同一分类方法比较', fontsize=16, fontweight='bold')  # 标注相同期间与分类方法
axes[1].set_yticks(x)  # 固定行业刻度位置
axes[1].set_yticklabels(industries,fontsize=14)  # 以横向标签显示完整行业名称
axes[1].legend(fontsize=16,title='基金代码')  # 显示两只真实基金代码
axes[1].grid(axis='x', alpha=0.3)  # 添加比例比较辅助网格
plt.tight_layout(rect=(0,.04,1,1))  # 为左侧外置分类图例保留底部空间
plt.show()  # 在当前单元发出完整图形
左侧饼图显示000001.OF前十大持仓经当前静态中信行业快照映射后占股票持仓市值比,并以其余股票持仓补足同一分母;右侧条形图按同口径比较000001.OF与000011.OF。该图不表示占基金净值比,且未验证报告期历史行业分类。
Figure 4: 两只中国基金股票持仓市值分母下的当前静态中信一级行业映射|持仓报告期2025-09-30

基金行业映射检查

Listing 3
展开来源、映射与守恒检查代码
audit_table=pd.DataFrame({'基金代码':stock_industry_share.columns,'持仓报告期':'2025-09-30','原始持仓行数':raw_holdings.groupby('ts_code').size().reindex(stock_industry_share.columns).to_numpy(),'证券覆盖率':mapped.groupby('ts_code')['citics_2019_l1_name'].apply(lambda values:values.notna().mean()).reindex(stock_industry_share.columns).to_numpy(),'占股票市值比合计':stock_industry_share.sum(axis=0).to_numpy()})  # 形成两基金同分母的映射与守恒检查表
print(audit_table.to_string(index=False))  # 可见输出两基金覆盖率与股票持仓市值分母内的守恒
print('持仓来源/访问日:',holdings_source)  # 可见输出持仓来源、公告日与访问日
print('持仓字段:',sorted(required),';代码规范化:SH→XSHG,SZ→XSHE')  # 可见输出原始字段与代码规范化
print('行业映射:',industry_file,';key=',industry_key,';字段=',sorted(map_required))  # 可见输出行业文件、key与字段结构
print('分类快照:',industry_snapshot)  # 可见输出分类口径与版本边界
print('分母边界:stk_mkv_ratio为占股票持仓市值比;其余股票持仓=100-前十大合计;不表示占基金净值比')  # 明示100%守恒的统计整体与禁止外推
print('PUBLICATION_STOP:真实比例摘录再分发授权未确认,公开发布前必须完成权利核对')  # 可见输出权利核对条件
     基金代码      持仓报告期  原始持仓行数  证券覆盖率  占股票市值比合计
000001.OF 2025-09-30      10    1.0     100.0
000011.OF 2025-09-30      10    1.0     100.0
持仓来源/访问日: Tushare fund_portfolio原始字段摘录;stk_mkv_ratio=占股票持仓市值比;公告日2025-10-28;访问日2026-08-26
持仓字段: ['ann_date', 'end_date', 'stk_mkv_ratio', 'symbol', 'ts_code'] ;代码规范化:SH→XSHG,SZ→XSHE
行业映射: /home/ubuntu/r2_data_mount/data/stock/stock_basic_data.h5 ;key= stock_basic_info ;字段= ['citics_2019_l1_name', 'order_book_id']
分类快照: citics_2019_l1_name(中信一级2019口径);当前静态文件快照访问日2026-08-26;用于映射2025-09-30历史持仓,未验证报告期历史分类
分母边界:stk_mkv_ratio为占股票持仓市值比;其余股票持仓=100-前十大合计;不表示占基金净值比
PUBLICATION_STOP:真实比例摘录再分发授权未确认,公开发布前必须完成权利核对

饼图使用的最佳实践

适合使用饼图的场景

  • 展示部分与整体的关系
  • 类别较少(≤6个)
  • 各部分比例差异明显

不适合使用饼图的场景

  • 类别过多(>8个)
  • 需要精确比较数值
  • 各部分比例相近(难以区分)

饼图设计技巧

  1. 排序:按大小顺时针排列,从12点开始
  2. 突出关键:用 explode 或颜色突出重点
  3. 颜色:使用对比色,避免彩虹色
  4. 标签:直接标注,避免图例来回对照
  5. 总数:在标题或中心说明总数值

本章小结

  • 饼图:展示部分与整体关系的经典工具
  • 环形图:更现代美观,中心可添加信息
  • 嵌套饼图:展示多层级数据结构
  • 选择原则:类别少用饼图,类别多用条形图
  • 关键参数autopctstartangleexplode

随堂练习

  • 问题 1|需要准备哪些数据?:Tushare原字段 ts_code/end_date/ann_date/symbol/stk_mkv_ratio、官方“占股票持仓市值比”分母及本地行业映射文件/key/字段/分类快照。
  • 问题 2|需要完成哪些操作?:证券代码规范化→当前静态行业快照映射历史持仓→逐基金聚合 stk_mkv_ratio→补齐其余股票持仓→选图→lst-industry-allocation-audit检查。
  • 问题 3|应得到哪些结果?:主基金与另一基金均输出占股票持仓市值比、其余股票持仓、映射覆盖率、同分母100%和、饼图与排序条形图。
  • 问题 4|怎样确认结果可靠?:检查文件和字段、证券与行业的匹配情况、比例是否为非负有限值,以及行业比例是否合计为 100%;同时说明当前行业分类与持仓报告期可能不完全一致,并确认数据公开使用权限。
  • 问题 5|换一个情境,怎样继续应用?:000011.OF 必须复用000001.OF完全相同的代码规范化、行业映射和聚合方法。
  • 作答提示:请依次写清所用数据、分析过程、所得结果、核对方法和拓展思考。课程所需数据见前言中的下载入口;教学平台固定题按页面说明完成。

教师参考解答|答案与说明 1

  • 所用数据与字段:Tushare原始持仓 ts_code/end_date/ann_date/symbol/stk_mkv_ratio,其中 stk_mkv_ratio 为占该基金股票持仓市值比;本地映射 order_book_id/citics_2019_l1_name;当前静态分类快照访问日2026-08-26,用于映射2025-09-30历史持仓,非报告期历史分类。

教师参考解答|代码 1

展开代码(代码区可独立滚动)
from pathlib import Path  # 导入路径工具以读取本地行业分类文件
import numpy as np  # 导入数值工具以检查有限值与守恒
import pandas as pd  # 导入表格工具以组织持仓原始字段
answer_file=Path('/home/ubuntu/r2_data_mount/data/stock/stock_basic_data.h5')  # 固定行业映射文件
answer_raw=pd.DataFrame([('000001.OF','20250930','20251028','603986.SH',2.69),('000001.OF','20250930','20251028','601689.SH',2.72),('000001.OF','20250930','20251028','300308.SZ',3.74),('000001.OF','20250930','20251028','002475.SZ',2.72),('000001.OF','20250930','20251028','002371.SZ',2.97),('000001.OF','20250930','20251028','688347.SH',3.80),('000001.OF','20250930','20251028','688041.SH',2.58),('000001.OF','20250930','20251028','002025.SZ',5.38),('000001.OF','20250930','20251028','688019.SH',2.49),('000001.OF','20250930','20251028','600276.SH',3.00),('000011.OF','20250930','20251028','300750.SZ',8.43),('000011.OF','20250930','20251028','300408.SZ',2.83),('000011.OF','20250930','20251028','002475.SZ',4.54),('000011.OF','20250930','20251028','601899.SH',3.26),('000011.OF','20250930','20251028','002371.SZ',2.86),('000011.OF','20250930','20251028','688052.SH',3.11),('000011.OF','20250930','20251028','688617.SH',2.69),('000011.OF','20250930','20251028','688088.SH',3.45),('000011.OF','20250930','20251028','600522.SH',3.05),('000011.OF','20250930','20251028','600276.SH',2.79)],columns=['ts_code','end_date','ann_date','symbol','stk_mkv_ratio'])  # 按官方字段名重新计算两基金持仓摘录
if not answer_file.exists() or not {'ts_code','end_date','ann_date','symbol','stk_mkv_ratio'}.issubset(answer_raw.columns): raise RuntimeError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 检查文件来源与字段
answer_raw['order_book_id']=answer_raw['symbol'].str.replace('.SH','.XSHG',regex=False).str.replace('.SZ','.XSHE',regex=False)  # 规范化官方symbol证券代码
answer_basic=pd.read_hdf(answer_file,key='stock_basic_info')  # 读取本地行业分类快照
if not {'order_book_id','citics_2019_l1_name'}.issubset(answer_basic.columns): raise RuntimeError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 检查行业分类字段
answer_map=answer_basic[['order_book_id','citics_2019_l1_name']].drop_duplicates('order_book_id')  # 形成唯一证券行业映射
answer_mapped=answer_raw.merge(answer_map,on='order_book_id',how='left',validate='many_to_one')  # 对两基金执行同一行业映射
if answer_mapped['citics_2019_l1_name'].isna().any() or not np.isfinite(answer_mapped['stk_mkv_ratio']).all(): raise RuntimeError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 执行资产覆盖与数值要求
answer_sum=answer_mapped.groupby(['ts_code','citics_2019_l1_name'])['stk_mkv_ratio'].sum()  # 聚合两基金前十大持仓占股票持仓市值比
answer_remaining_stock=100-answer_sum.groupby(level=0).sum()  # 在同一股票市值分母内计算其余股票持仓
answer_sum=pd.concat([answer_sum,pd.Series(answer_remaining_stock.values,index=pd.MultiIndex.from_arrays([answer_remaining_stock.index,['其余股票持仓']*len(answer_remaining_stock)]))])  # 补齐两基金股票持仓市值整体
answer_stock_share=answer_sum.unstack(0).fillna(0.0)  # 形成逐基金占股票持仓市值比矩阵
assert np.allclose(answer_stock_share.sum(axis=0),100.0) and (answer_stock_share>=0).all().all()  # 检查两基金在股票持仓市值分母内守恒与非负
print({'来源':'Tushare fund_portfolio字段摘录;访问日2026-08-26','官方字段':'stk_mkv_ratio','分母':'占股票持仓市值比','持仓报告期':'2025-09-30','行业文件':str(answer_file),'key':'stock_basic_info','分类字段':'citics_2019_l1_name','分类边界':'当前静态快照映射历史持仓;未验证报告期历史分类','证券覆盖率':answer_mapped['citics_2019_l1_name'].notna().mean(),'同分母比例和':answer_stock_share.sum().to_dict()})  # 输出可核对本章计算与分母边界

教师参考解答|答案与说明 2

  • 解释答案:饼图适合非负且总和明确的少量构成;本例的统计整体仅是股票持仓市值,“其余股票持仓”不包含现金、债券或其他资产。类别多且余额大,因此排序条形图是精确比较主依据,饼图只展示同分母构成。
  • 拓展应用答案|另一只中国基金公开披露快照

教师参考解答|代码 2

展开代码(代码区可独立滚动)
import matplotlib.pyplot as plt  # 导入绘图工具以生成新案例结果
alternative_fund=answer_stock_share['000011.OF']  # 从同一股票持仓市值分母结果选择另一基金
assert '000001.OF' in answer_stock_share and alternative_fund.name!='000001.OF'  # 检查新案例对象不同于主基金
assert np.isfinite(alternative_fund).all() and (alternative_fund>=0).all() and np.isclose(alternative_fund.sum(),100.0)  # 核对新案例有限值、非负与守恒
fig,axes=plt.subplots(1,2,figsize=(9,3.6))  # 创建新案例基金双面板
alternative_fund.sort_values().plot.barh(ax=axes[0],title='000011.OF|占股票市值比')  # 生成新案例基金同分母排序行业图
alternative_fund.plot.pie(ax=axes[1],autopct='%.1f%%',ylabel='',title='000011.OF|占股票市值比')  # 生成新案例基金同分母构成饼图
plt.tight_layout(); plt.show()  # 显示另一中国基金新案例结果
print({'新案例基金':alternative_fund.name,'行业占股票市值比':alternative_fund.to_dict(),'同分母比例和':alternative_fund.sum(),'分母说明':'100-前十大合计仅为其余股票持仓,非占基金净值比','分类说明':'当前静态快照用于对应历史持仓','检查项目':['文件','字段','证券覆盖','有限值','非负','同分母守恒'],'使用说明':'引用或转发真实比例前请确认数据授权'})  # 输出新案例结果和使用说明

教师参考解答|答案与说明 3

  • 参考结果:两基金官方原字段→代码规范化→当前静态行业快照映射历史持仓→聚合 stk_mkv_ratio→在股票持仓市值分母内100%守恒,以及排序条形图与饼图。另行核对:证券映射覆盖率100%;两基金“前十大+其余股票持仓”在同分母内为100%,不作基金净值分母解释。
  • 常见错误:把 stk_mkv_ratio 误称占净值比;把余额扩展为现金/债券等其他资产;把当前静态分类快照冒充报告期历史分类;另一基金改用不同方法;忽略PUBLICATION_STOP。